﻿namespace FastColoredTextBoxNS
{
    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Windows.Forms;

    /// <summary>
    /// 	Shows document map of FCTB
    /// </summary>
    public class DocumentMap : Control
    {
        public EventHandler TargetChanged;

        private FastColoredTextBox target;

        private float scale = 0.3f;

        private bool needRepaint = true;

        private Place startPlace = Place.Empty;

        private bool scrollbarVisible = true;

        [Description("Target FastColoredTextBox")]
        public FastColoredTextBox Target
        {
            get
            {
                return this.target;
            }
            set
            {
                if (this.target != null)
                {
                    this.UnSubscribe(this.target);
                }

                this.target = value;
                if (value != null)
                {
                    this.Subscribe(this.target);
                }
                this.OnTargetChanged();
            }
        }

        /// <summary>
        /// 	Scale
        /// </summary>
        [Description("Scale")]
        [DefaultValue(0.3f)]
        public float Scale
        {
            get
            {
                return this.scale;
            }
            set
            {
                this.scale = value;
                this.NeedRepaint();
            }
        }

        /// <summary>
        /// 	Scrollbar visibility
        /// </summary>
        [Description("Scrollbar visibility")]
        [DefaultValue(true)]
        public bool ScrollbarVisible
        {
            get
            {
                return this.scrollbarVisible;
            }
            set
            {
                this.scrollbarVisible = value;
                this.NeedRepaint();
            }
        }

        public DocumentMap()
        {
            this.ForeColor = Color.Maroon;
            this.SetStyle(
                ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint
                | ControlStyles.ResizeRedraw,
                true);
            Application.Idle += this.Application_Idle;
        }

        private void Application_Idle(object sender, EventArgs e)
        {
            if (this.needRepaint)
            {
                this.Invalidate();
            }
        }

        protected virtual void OnTargetChanged()
        {
            this.NeedRepaint();

            if (this.TargetChanged != null)
            {
                this.TargetChanged(this, EventArgs.Empty);
            }
        }

        protected virtual void UnSubscribe(FastColoredTextBox target)
        {
            target.Scroll -= this.Target_Scroll;
            target.SelectionChangedDelayed -= this.Target_SelectionChanged;
            target.VisibleRangeChanged -= this.Target_VisibleRangeChanged;
        }

        protected virtual void Subscribe(FastColoredTextBox target)
        {
            target.Scroll += this.Target_Scroll;
            target.SelectionChangedDelayed += this.Target_SelectionChanged;
            target.VisibleRangeChanged += this.Target_VisibleRangeChanged;
        }

        protected virtual void Target_VisibleRangeChanged(object sender, EventArgs e)
        {
            this.NeedRepaint();
        }

        protected virtual void Target_SelectionChanged(object sender, EventArgs e)
        {
            this.NeedRepaint();
        }

        protected virtual void Target_Scroll(object sender, ScrollEventArgs e)
        {
            this.NeedRepaint();
        }

        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            this.NeedRepaint();
        }

        public void NeedRepaint()
        {
            this.needRepaint = true;
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            if (this.target == null)
            {
                return;
            }

            float zoom = this.Scale * 100 / this.target.Zoom;

            if (zoom <= float.Epsilon)
            {
                return;
            }

            //calc startPlace
            Range r = this.target.VisibleRange;
            if (this.startPlace.iLine > r.Start.iLine)
            {
                this.startPlace.iLine = r.Start.iLine;
            }
            else
            {
                Point endP = this.target.PlaceToPoint(r.End);
                endP.Offset(0, -(int)(this.ClientSize.Height / zoom) + this.target.CharHeight);
                Place pp = this.target.PointToPlace(endP);
                if (pp.iLine > this.startPlace.iLine)
                {
                    this.startPlace.iLine = pp.iLine;
                }
            }
            this.startPlace.iChar = 0;
            //calc scroll pos
            int linesCount = this.target.Lines.Count;
            float sp1 = (float)r.Start.iLine / linesCount;
            float sp2 = (float)r.End.iLine / linesCount;

            //scale graphics
            e.Graphics.ScaleTransform(zoom, zoom);
            //draw text
            var size = new SizeF(this.ClientSize.Width / zoom, this.ClientSize.Height / zoom);
            this.target.DrawText(e.Graphics, this.startPlace, size.ToSize());

            //draw visible rect
            Point p0 = this.target.PlaceToPoint(this.startPlace);
            Point p1 = this.target.PlaceToPoint(r.Start);
            Point p2 = this.target.PlaceToPoint(r.End);
            int y1 = p1.Y - p0.Y;
            int y2 = p2.Y + this.target.CharHeight - p0.Y;

            e.Graphics.SmoothingMode = SmoothingMode.HighQuality;

            using (var brush = new SolidBrush(Color.FromArgb(50, this.ForeColor)))
            using (var pen = new Pen(brush, 1 / zoom))
            {
                var rect = new Rectangle(0, y1, (int)((this.ClientSize.Width - 1) / zoom), y2 - y1);
                e.Graphics.FillRectangle(brush, rect);
                e.Graphics.DrawRectangle(pen, rect);
            }

            //draw scrollbar
            if (this.scrollbarVisible)
            {
                e.Graphics.ResetTransform();
                e.Graphics.SmoothingMode = SmoothingMode.None;

                using (var brush = new SolidBrush(Color.FromArgb(200, this.ForeColor)))
                {
                    var rect = new RectangleF(
                        this.ClientSize.Width - 3, this.ClientSize.Height * sp1, 2, this.ClientSize.Height * (sp2 - sp1));
                    e.Graphics.FillRectangle(brush, rect);
                }
            }

            this.needRepaint = false;
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                this.Scroll(e.Location);
            }
            base.OnMouseDown(e);
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                this.Scroll(e.Location);
            }
            base.OnMouseMove(e);
        }

        private void Scroll(Point point)
        {
            if (this.target == null)
            {
                return;
            }

            float zoom = this.Scale * 100 / this.target.Zoom;

            if (zoom <= float.Epsilon)
            {
                return;
            }

            Point p0 = this.target.PlaceToPoint(this.startPlace);
            p0 = new Point(0, p0.Y + (int)(point.Y / zoom));
            Place pp = this.target.PointToPlace(p0);
            this.target.DoRangeVisible(new Range(this.target, pp, pp), true);
            this.BeginInvoke((MethodInvoker)this.OnScroll);
        }

        private void OnScroll()
        {
            this.Refresh();
            this.target.Refresh();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                Application.Idle -= this.Application_Idle;
                if (this.target != null)
                {
                    this.UnSubscribe(this.target);
                }
            }
            base.Dispose(disposing);
        }
    }
}